/**
 * nMuxer, the semi-sequel to nPlayer.
 * Now with PerfectSync2.
 */
if (!window.getXHRObject) {
    window.getXHRObject = function() {
        try {
            return new XMLHttpRequest();
        }catch(e) {}
        try {
            return new ActiveXObject("Msxml3.XMLHTTP");
        }catch(e) {}
        try {
            return new ActiveXObject("Msxml2.XMLHTTP.6.0");
        }catch(e) {}
        try {
            return new ActiveXObject("Msxml2.XMLHTTP.3.0");
        }catch(e) {}
        try {
            return new ActiveXObject("Msxml2.XMLHTTP");
        }catch(e) {}
        try {
            return new ActiveXObject("Microsoft.XMLHTTP");
        }catch(e) {}
        return null;
    }
}
if (!window.sleep) {
    window.sleep = function(ms) {
        var unixtime_ms = new Date().getTime();
        while(new Date().getTime() < unixtime_ms + ms) {}
    }
}
if (!window.base64Encode) {
    function base64Encode(str) {
        try {
             return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) {
                return String.fromCharCode(parseInt(p1, 16))
            }));
          }catch (ex) {
              return btoa(str);
         }
    }
}
// Create a constructor for the fixed-length queue. This is
// really more of a FACTORY than a construtor since an
// entirely tangential object is returned.
function FixedQueue(size, initialValues) {

    // If there are no initial arguments, default it to
    // an empty value so we can call the constructor in
    // a uniform way.
    initialValues = (initialValues || []);

    // Create the fixed queue array value.
    var queue = Array.apply(null, initialValues);

    // Store the fixed size in the queue.
    queue.fixedSize = size;

    // Add the class methods to the queue. Some of these have
    // to override the native Array methods in order to make
    // sure the queue lenght is maintained.
    queue.push = FixedQueue.push;
    queue.splice = FixedQueue.splice;
    queue.unshift = FixedQueue.unshift;

    // Trim any initial excess from the queue.
    FixedQueue.trimTail.call(queue);

    // Return the new queue.
    return (queue);

}


// I trim the queue down to the appropriate size, removing
// items from the beginning of the internal array.
FixedQueue.trimHead = function () {

    // Check to see if any trimming needs to be performed.
    if (this.length <= this.fixedSize) {

        // No trimming, return out.
        return;

    }

    // Trim whatever is beyond the fixed size.
    Array.prototype.splice.call(
        this,
        0,
        (this.length - this.fixedSize)
    );

};

FixedQueue.isFull = function() {
    return this.length >= this.fixedSize;
}

// I trim the queue down to the appropriate size, removing
// items from the end of the internal array.
FixedQueue.trimTail = function () {

    // Check to see if any trimming needs to be performed.
    if (this.length <= this.fixedSize) {

        // No trimming, return out.
        return;

    }

    // Trim whatever is beyond the fixed size.
    Array.prototype.splice.call(
        this,
        this.fixedSize,
        (this.length - this.fixedSize)
    );

};


// I synthesize wrapper methods that call the native Array
// methods followed by a trimming method.
FixedQueue.wrapMethod = function (methodName, trimMethod) {

    // Create a wrapper that calls the given method.
    var wrapper = function () {

        // Get the native Array method.
        var method = Array.prototype[methodName];

        // Call the native method first.
        var result = method.apply(this, arguments);

        // Trim the queue now that it's been augmented.
        trimMethod.call(this);

        // Return the original value.
        return (result);

    };

    // Return the wrapper method.
    return (wrapper);

};


// Wrap the native methods.
FixedQueue.push = FixedQueue.wrapMethod(
    "push",
    FixedQueue.trimHead
);

FixedQueue.splice = FixedQueue.wrapMethod(
    "splice",
    FixedQueue.trimTail
);

FixedQueue.unshift = FixedQueue.wrapMethod(
    "unshift",
    FixedQueue.trimTail
);
FixedQueue.isFull = FixedQueue.wrapMethod(
    "isFull",
    FixedQueue.isFull
);
const nMuxerUtils = {
    hasMediaSource: function() {
        if (window.MediaSource) {
            return true;
        }else if (window.webkitMediaSource) {
            return true;
        }else {
            return false;
        }
    },
    getMediaSource: function() {
        if (window.MediaSource) {
            return new window.MediaSource();
        }else if (window.webkitMediaSource) {
            return new window.webkitMediaSource();
        }else {
            return null;
        }
    },
    getSegmentSizeForNetworkSpeed: function() {
        if (navigator.connection
            && navigator.connection.downlink) {
            const LIMIT = 60 * 81920;
            var chunkSize = navigator.connection.downlink * 81920;
            if (chunkSize > LIMIT) {
                chunkSize = LIMIT;
            }
            const HIGHER_CHUNK_RTT_LIMIT = 850;
            if (navigator.connection.rtt > HIGHER_CHUNK_RTT_LIMIT) {
                chunkSize /= 1.4;
                if (chunkSize % 2 !== 0) {
                    while (chunkSize % 2 !== 0) {
                        chunkSize -= 1;
                    }
                }
            }
            if (navigator.connection.effectiveType === "4g") {
                chunkSize *= 2;
                if (chunkSize % 2 !== 0) {
                    while (chunkSize % 2 !== 0) {
                        chunkSize += 1;
                    }
                }
            }
            console.debug("[nMuxerUtils::getSegmentSizeForNetworkSpeed] Chunk size: " + nMuxerUtils.formatBytes(chunkSize));
            return chunkSize;
        }else {
            console.warn("[nMuxerUtils::getSegmentSizeForNetworkSpeed] NetworkInformation API is missing, returning defaults!");
            console.debug("[nMuxerUtils::getSegmentSizeForNetworkSpeed] Chunk size: " + nMuxerUtils.formatBytes(253952));
            return 253952;
        }
    },
    formatBytes: function(bytes) {
        nMuxerUtils.formatBytes(bytes, 2);
    },
    formatBytes: function(bytes, decimals) {
        if (bytes === 0) return '0 B';
        const k = 1024;
        const dm = decimals < 0 ? 0 : decimals;
        const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
        const i = Math.floor(Math.log(bytes) / Math.log(k));
        return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
    },
    getMaximumBufferSize: function() {
        const isMobile = navigator.userAgent.contains("Mobi") || navigator.userAgent.contains("Android") || navigator.userAgent.contains("iOS");
        if (navigator.userAgent.contains("CrKey")) {
            return 33554432; // 32 MiB, Chromecasts
        }else if (isMobile) {
            return 47185920; // 45 MiB, Chromium Mobile/Firefox Mobile
        }else if (typeof InstallTrigger !== 'undefined') {
            return 136314880; // 130 MiB, Firefox
        }else {
            return 125829120; // 120 MiB, Chromium/Other
        }
    },
    doRequest: function(url, useProxy, rangePayload, onSegment) {
        function _doRequest(url, useProxy, rangePayload, onSegment) {
            const useAltPayloadMethod = false; //url.indexOf("googlevideo") > -1;
            var request = window.getXHRObject();
            request.responseType = 'arraybuffer';
            request.onload = function () {
                onSegment(request.response);
            };
            if (useAltPayloadMethod) {
                url += "&range=" + rangePayload;
            }else {
            }
            //if (useProxy) {
            //    url = "/proxyAPI?url=" + base64Encode(url);
            //}
            request.open('GET', url);
            if (!useAltPayloadMethod) {
                request.setRequestHeader("Range", "bytes=" + rangePayload);
            }
            request.send();
        }
        if (window.run_promise) {
            run_promise(function() {
                _doRequest(url, useProxy, rangePayload, onSegment);
            });
        }else {
            _doRequest(url, useProxy, rangePayload, onSegment);
        }
    },
    run_promise: function(func) {
        if (window.scheduler && window.scheduler.postTask) {
            return window.scheduler.postTask(func).result;
        }else if (window.Promise) {
            return new window.Promise(func);
        }else {
            return ((function() {
                var n = func();
                return {
                    value: n,
                    then: function(y) {
                        y(n);
                    }
                }
            })());
        }
    }
}
/*
    initData -> [
        {
            "url": $URL
            "bitrate": bitrate
            "coreData": {
                "init": $INIT,
                "index": $INDEX
            },
            length: $CLEN
            mime: $MIME
            height: $HEIGHT [UNIMPLEMENTED]
        }
    ]
 */
const nMuxer = function(initData) {
    if (!nMuxerUtils.hasMediaSource()) {
        throw new Error("nMuxer: Support for MediaSource is missing! (MSE is required)");
    }
    const maxBufferSize = nMuxerUtils.getMaximumBufferSize();
    var SEGMENT_SIZE = nMuxerUtils.getSegmentSizeForNetworkSpeed();
    var fullSize = 0;
    for (var i = 0; i < initData.length; i++) {
        initData[i]["muxer_data"] = {};
        initData[i]["muxer_data"]["remaining"] = initData[i]["length"];
        initData[i]["muxer_data"]["read"] = 0;
        var init = initData[i]["coreData"]["init"];
        var index = initData[i]["coreData"]["index"];
        var initChunk = init.split("-")[0] + "-" + index.split("-")[1];
        initData[i]["muxer_data"]["initChunk"] = initChunk;
        initData[i]["muxer_data"]["initLength"] = parseInt(index.split("-")[1]);
        fullSize += initData[i]["length"];
    }
    const canLazyLoad = fullSize <= maxBufferSize;
    if (canLazyLoad) {
        console.debug("[nMuxer] Total length size does not exceed buffer limit, lazily loading all content.");
        console.debug("bufferLimit: " + maxBufferSize + " (" + nMuxerUtils.formatBytes(maxBufferSize) + ")");
        console.debug("fullSize: " + fullSize + " (" + nMuxerUtils.formatBytes(fullSize) + ")");
    }
    var ms = nMuxerUtils.getMediaSource();
    ms.addEventListener('sourceopen', function(_) {
        console.debug("[nMuxer] sourceOpen called!")
        var completedInits = 0;
        for (var sc = 0; sc < initData.length; sc++) {
            const i = sc;
            const index = i;
            console.debug("[nMuxer] Running for initData[" + i + "].");
            nMuxerUtils.run_promise(function() {
                const muxerData = initData[index].muxer_data;
                function updateLengths(length) {
                    muxerData["remaining"] -= length;
                    muxerData["read"] += length;
                }
                const id = initData[i];
                muxerData["sourceBuffer"] = ms.addSourceBuffer(id["mime"]);
                const sb = muxerData["sourceBuffer"];
                console.debug("[nMuxer:" + index + "] Created sourceBuffer for '" + id["mime"] + "'.");
                console.debug("[nMuxer:" + index + "] Loading init segment for codec.");
                const initLength = muxerData["initLength"] + 1;
                console.debug("initLength: " + muxerData["initLength"] + ", parsed: " + initLength);
                const initSegment = "0-" + initLength;
                var reading = initLength;
                function getNextChunk() {
                    if (muxerData["remaining"] <= 0) return {from: null, to: null, end: true};
                    var from = muxerData["read"] + 1;
                    var to = muxerData["read"] + SEGMENT_SIZE + 1;
                    console.debug("[nMuxer:" + index + "] getNextChunk: {from: " + from + ", to: " + to + "} -> remaining-post-op: " + (muxerData["remaining"] - (to - from)));
                    if (muxerData["remaining"] - (to - from) < 0) {
                        console.debug("getNextChunk: Cancelled " + (to - from) + " byte read.")
                        return {from: null, to: null, end: true};
                    }
                    updateLengths(to - from);
                    return {from: from, to: to};
                }
                console.debug("[nMuxer:" + index + "] Loading first chunk.")
                nMuxerUtils.doRequest(id["url"], true, initSegment, function(segment) {
                    sb.appendBuffer(segment);
                    console.debug("[nMuxer] init(" + index + ") -> segment: " + (0 + "-" + initLength));
                    updateLengths(initLength);
                    if (canLazyLoad) {
                        console.debug("[nMuxer] Lazily loading the rest of video.");
                        const queue = FixedQueue(8);
                        muxerData["queue"] = queue;
                        sb.onupdateend = function() {
                            if (queue.length > 0) {
                                console.debug("Queue[" + index + "]: length: " + queue.length);
                                if (sb.appendBufferAsync) {
                                    sb.appendBufferAsync(queue.shift());
                                }else {
                                    sb.appendBuffer(queue.shift());
                                }
                            }else {
                                if (muxerData["remaining"] > -1) {
                                    console.debug("[nMuxer:" + index + "] Buffer is empty! Requesting more buffers.");
                                    console.debug("Queue[" + index + "]: remaining: " + muxerData["remaining"]);
                                    for (var i = 0; i < 8 - queue.length; i++) {
                                        const nextChunk = getNextChunk();
                                        if (nextChunk.end) {
                                            break;
                                        }else {
                                            load(nextChunk.from, nextChunk.to);
                                        }
                                    }
                                }else {
                                    console.debug("[nMuxer:" + index + "] END OF BUFFER: " + index);
                                    completedInits += 1;
                                    if (completedInits == initData.length) {
                                        try {
                                            (async function() {
                                                for (var buffer of ms.activeSourceBuffers) {
                                                    await (() => buffer.updating === false);
                                                }
                                                ms.endOfStream();
                                            })();
                                        }catch (e) {
                                            console.error(ms);
                                            console.error(e);
                                        }
                                    }
                                }
                            }
                        };
                        function postOperation(from, to) {
                            console.debug("[nMuxer:" + index + "] remaining: " + muxerData["remaining"] + ", read: " + muxerData["read"]);
//                          const nextChunk = getNextChunk();
                            if (muxerData["remaining"] > -1) {
                            }else {
                                completedInits += 1;
                                console.debug("[nMuxer:" + index + "] Finished loading for stream " + i + ".")
                            }
                        }
                        function load(from, to) {
                            console.debug("[nMuxer:" + index + "] load(" + index + ") -> segment: " + (from + "-" + to));
                            nMuxerUtils.doRequest(id["url"], true, from + "-" + to, function(_segment) {
                                console.debug("[doRequest:" + index + "] segment: " + _segment.byteLength);
                                queue.push(_segment);
                                if (!sb.updating) {
                                    console.warn("[nMuxer:" + index + "] Attempting to restart queue " + index + ".");
                                    sb.onupdateend(null);
                                }
                                postOperation(from, to);
                            });
                        }
                        const nextChunk = getNextChunk();
                        if (nextChunk.end) return;
                        load(nextChunk.from, nextChunk.to);
                    }
                });
            });
        }
    });
    const s = {
        "version": "infdev-01072020",
        "mediaSource": ms,
        "initData": initData,
    };
    return s;
}
nMuxer.version = "infdev-01072020";
window.nMuxer = nMuxer;